ATOM Documentation

← Back to App

Authentication & Error Handling Standard

Overview

This document defines the standard patterns for authentication and error handling in API routes. Consistent patterns improve security, user experience, and maintainability.

Authentication Standard

Helper Location

**File**: src/lib/auth/get-authenticated-user.ts

Standard Pattern

import { getAuthenticatedUser, AuthError } from '@/lib/auth/get-authenticated-user'
import { sendApiError, sendApiSuccess, withApiHandler } from '@/lib/api/api-response'

// ✅ GOOD: Using helper
export async function GET(request: Request) {
  return withApiHandler(async () => {
    const user = await getAuthenticatedUser(request as NextRequest)

    if (!user) {
      throw new AuthError('Unauthorized', 'UNAUTHORIZED', 401)
    }

    // User is authenticated, proceed with logic
    return sendApiSuccess({ userId: user.id })
  })
}

Authentication Methods

1. Get Authenticated User

import { getAuthenticatedUser } from '@/lib/auth/get-authenticated-user'

const user = await getAuthenticatedUser(request)

if (!user) {
  return sendApiError(401, 'Unauthorized', 'UNAUTHORIZED')
}

// User is authenticated
console.log(user.id, user.email)

2. Get User or Throw

import { getAuthenticatedUserOrThrow } from '@/lib/auth/get-authenticated-user'

try {
  const user = await getAuthenticatedUserOrThrow(request)
  // User is guaranteed to be authenticated
  console.log(user.id)
} catch (error) {
  if (error instanceof AuthError) {
    return sendApiError(error.statusCode, error.message, error.code)
  }
}

3. Require Auth

import { requireAuth } from '@/lib/auth/get-authenticated-user'

export async function GET(request: Request) {
  return withApiHandler(async () => {
    const user = await requireAuth(request as NextRequest)
    // User is guaranteed to be authenticated
    return sendApiSuccess({ userId: user.id })
  })
}

4. Require Role

import { requireRole } from '@/lib/auth/get-authenticated-user'

export async function GET(request: Request) {
  return withApiHandler(async () => {
    const user = await requireRole(request as NextRequest, 'admin')
    // User is guaranteed to be admin
    return sendApiSuccess({ userId: user.id })
  })
}

5. Require Any Role

import { requireAnyRole } from '@/lib/auth/get-authenticated-user'

export async function GET(request: Request) {
  return withApiHandler(async () => {
    const user = await requireAnyRole(request as NextRequest, ['admin', 'owner'])
    // User is guaranteed to be admin or owner
    return sendApiSuccess({ userId: user.id })
  })
}

Before vs After

Before (Inconsistent)

// ❌ BAD: Manual session check
export async function GET(req: NextRequest) {
  const session = await getServerSession(authOptions)
  if (!session) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    )
  }

  // ... rest of logic
}

// ❌ BAD: Different error format
export async function POST(req: NextRequest) {
  const session = await getServerSession(authOptions)
  if (!session || !session.user) {
    return NextResponse.json(
      { error: 'UNAUTHORIZED', code: 'AUTH_FAILED' },
      { status: 401 }
    )
  }

  // ... rest of logic
}

After (Standard)

// ✅ GOOD: Using helper with standard error
import { requireAuth } from '@/lib/auth/get-authenticated-user'
import { withApiHandler, sendApiSuccess } from '@/lib/api/api-response'

export async function GET(request: Request) {
  return withApiHandler(async () => {
    const user = await requireAuth(request as NextRequest)
    return sendApiSuccess({ userId: user.id })
  })
}

export async function POST(request: Request) {
  return withApiHandler(async () => {
    const user = await requireAuth(request as NextRequest)
    return sendApiSuccess({ userId: user.id })
  })
}

Error Handling Standard

Helper Location

**File**: src/lib/api/api-response.ts

Standard Pattern

import { withApiHandler, sendApiError, sendApiSuccess, Errors } from '@/lib/api/api-response'

// ✅ GOOD: Using withApiHandler wrapper
export async function GET(request: Request) {
  return withApiHandler(async () => {
    // All errors automatically caught and formatted
    const data = await someOperation()
    return sendApiSuccess(data)
  })
}

Error Handling Methods

1. Using withApiHandler

import { withApiHandler, sendApiSuccess } from '@/lib/api/api-response'

export async function GET(request: Request) {
  return withApiHandler(async () => {
    // Errors automatically caught and formatted
    const result = await getData()
    return sendApiSuccess(result)
  })
}

2. Throwing ApiError

import { withApiHandler, ApiError, sendApiSuccess } from '@/lib/api/api-response'

export async function GET(request: Request) {
  return withApiHandler(async () => {
    const data = await getData()

    if (!data) {
      throw new ApiError(404, 'NOT_FOUND', 'Data not found')
    }

    return sendApiSuccess(data)
  })
}

3. Using Errors Object

import { withApiHandler, Errors, sendApiSuccess } from '@/lib/api/api-response'

export async function POST(request: Request) {
  return withApiHandler(async () => {
    const body = await request.json()

    if (!body.name) {
      throw Errors.badRequest('Name is required')
    }

    const result = await createData(body)
    return sendApiSuccess(result, 201)
  })
}

4. Manual Error Handling

import { handleApiError, sendApiSuccess } from '@/lib/api/api-response'

export async function GET(request: Request) {
  try {
    const result = await getData()
    return sendApiSuccess(result)
  } catch (error) {
    return handleApiError(error, 'GET /api/data')
  }
}

Common Error Types

import { Errors } from '@/lib/api/api-response'

// Authentication errors
throw Errors.unauthorized('You must log in')
throw Errors.forbidden('Access denied')

// Validation errors
throw Errors.badRequest('Invalid input')
throw Errors.validation({ field: 'error' })

// Resource errors
throw Errors.notFound('User')
throw Errors.conflict('Resource already exists')

// Rate limiting
throw Errors.rateLimited()

// Payment errors
throw Errors.paymentRequired('Upgrade required')

// Server errors
throw Errors.internal('Something went wrong')

Complete Example

import {
  withApiHandler,
  withTenantContext,
  sendApiSuccess,
  Errors
} from '@/lib/api/api-response'
import { requireAuth } from '@/lib/auth/get-authenticated-user'

export async function GET(request: Request) {
  return withApiHandler(async () => {
    // Authentication
    const user = await requireAuth(request as NextRequest)

    // Tenant context
    return withTenantContext(async ({ id: tenantId }) => {
      // Business logic
      const agents = await getAgents(tenantId)

      // Success response
      return sendApiSuccess(agents)
    }, request)
  })
}

export async function POST(request: Request) {
  return withApiHandler(async () => {
    // Authentication
    const user = await requireAuth(request as NextRequest)

    // Validation
    const body = await request.json()

    if (!body.name) {
      throw Errors.badRequest('Name is required')
    }

    // Tenant context
    return withTenantContext(async ({ id: tenantId }) => {
      // Business logic
      const result = await createAgent(tenantId, body)

      // Success response
      return sendApiSuccess(result, 201)
    }, request)
  })
}

Best Practices

1. Always Use withApiHandler

// ✅ GOOD: Errors automatically handled
export async function GET(request: Request) {
  return withApiHandler(async () => {
    const data = await getData()
    return sendApiSuccess(data)
  })
}

// ❌ BAD: Manual error handling
export async function GET(request: Request) {
  try {
    const data = await getData()
    return sendApiSuccess(data)
  } catch (error) {
    return handleApiError(error, 'GET')
  }
}

2. Use Auth Helpers

// ✅ GOOD: Using helper
const user = await requireAuth(request)

// ❌ BAD: Manual session check
const session = await getServerSession(authOptions)
if (!session) {
  return sendApiError(401, 'Unauthorized')
}

3. Include Error Codes

// ✅ GOOD: Descriptive error code
throw Errors.notFound('Agent', { agentId: '123' })
// Returns: { error: 'Agent not found', code: 'NOT_FOUND', details: {...} }

// ❌ BAD: Generic error
throw new Error('Not found')
// Returns: { error: 'Not found', code: 'INTERNAL_ERROR' }

4. Use Tenant Context Helper

// ✅ GOOD: Tenant context guaranteed
return withTenantContext(async ({ id: tenantId }) => {
  // Guaranteed to have tenant
  const data = await getData(tenantId)
  return sendApiSuccess(data)
}, request)

// ❌ BAD: Manual tenant extraction
const tenant = await getTenantFromRequest(request)
if (!tenant) {
  return sendApiError(404, 'Tenant not found')
}
const data = await getData(tenant.id)

5. Chain Wrappers

// ✅ GOOD: Chained wrappers
export async function POST(request: Request) {
  return withApiHandler(async () => {
    return withTenantContext(async ({ id: tenantId }) => {
      return withRateLimit(async () => {
        const result = await doSomething(tenantId)
        return sendApiSuccess(result)
      }, tenantId, redis)
    }, request)
  })
}

Migration Guide

Before

import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'

export async function GET(req: NextRequest) {
  try {
    const session = await getServerSession(authOptions)
    if (!session) {
      return NextResponse.json(
        { error: 'Unauthorized' },
        { status: 401 }
      )
    }

    const tenant = await getTenantFromRequest(req)
    if (!tenant) {
      return NextResponse.json(
        { error: 'Tenant not found' },
        { status: 404 }
      )
    }

    const data = await getData(tenant.id)

    return NextResponse.json(data)
  } catch (error) {
    console.error('Error:', error)
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    )
  }
}

After

import {
  withApiHandler,
  withTenantContext,
  sendApiSuccess,
  Errors
} from '@/lib/api/api-response'
import { requireAuth } from '@/lib/auth/get-authenticated-user'

export async function GET(request: Request) {
  return withApiHandler(async () => {
    return withTenantContext(async ({ id: tenantId }) => {
      const data = await getData(tenantId)
      return sendApiSuccess(data)
    }, request)
  })
}

Testing

Test Authentication

import { requireAuth } from '@/lib/auth/get-authenticated-user'

test('requires authentication', async () => {
  const request = new Request('http://localhost')

  await expect(requireAuth(request)).rejects.toThrow('Unauthorized')
})

Test Error Handling

import { withApiHandler, Errors } from '@/lib/api/api-response'

test('handles errors', async () => {
  const handler = withApiHandler(async () => {
    throw Errors.notFound('Resource')
  })

  const response = await handler(new Request('http://localhost'))
  const data = await response.json()

  expect(response.status).toBe(404)
  expect(data.code).toBe('NOT_FOUND')
})

Security

Always Check Authentication

// ✅ GOOD: Auth check required
export async function DELETE(request: Request) {
  return withApiHandler(async () => {
    const user = await requireAuth(request)
    // Safe to proceed
    await deleteSomething(user.id)
    return sendApiSuccess({ success: true })
  })
}

// ❌ BAD: No auth check
export async function DELETE(request: Request) {
  return withApiHandler(async () => {
    // Anyone can delete!
    await deleteSomething(anyoneId)
    return sendApiSuccess({ success: true })
  })
}

Use Tenant Isolation

// ✅ GOOD: Tenant isolation guaranteed
return withTenantContext(async ({ id: tenantId }) => {
  const data = await db.query(
    'SELECT * FROM agents WHERE tenant_id = $1',
    [tenantId]
  )
  return sendApiSuccess(data)
}, request)

// ❌ BAD: No tenant isolation
const data = await db.query('SELECT * FROM agents')
// Returns all tenants' data!

References

  • Auth Helper: src/lib/auth/get-authenticated-user.ts
  • API Response: src/lib/api/api-response.ts
  • NextAuth: https://next-auth.js.org/
  • API Standard: docs/API_RESPONSE_STANDARD.md

Changelog

  • 2026-02-08: Initial standard created
  • 2026-02-08: Auth helper implemented
  • 2026-02-08: Error handling patterns documented